﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.IO;
using System.Threading;

namespace GW2LastOnline.Services
{
    class Achievements
    {
        public enum Region : byte { EU, NA };

        private const string REGION_NA = "na";
        private const string REGION_EU = "eu";

        private const byte ERROR_RETRY_LIMIT = 3;
        private const byte ERROR_TIMEOUT_SECONDS = 3;

        private const string ACHIEVEMENTS_URL = "https://leaderboards.guildwars2.com/en/{0}/achievements";
        private const string WORLD_URL = "https://leaderboards.guildwars2.com/en/{0}/achievements/world/{1}?pajx=1&page={2}";
        private const string GUILD_URL = "https://leaderboards.guildwars2.com/en/{0}/achievements/guild/{1}?pjax=1&page={2}";
        private const string FRIENDS_URL = "https://leaderboards.guildwars2.com/en/{0}/achievements/friends?pjax=1&page={1}";

        public delegate void OnAchievementsPageUpdatedEventHandler(AchievementsPage page);
        public delegate AchievementsPage GetAchievementsPageDelegate(short page);
        public delegate void OnErrorEventHandler(LeaderboardException e);

        public event OnAchievementsPageUpdatedEventHandler OnAchievementsPageUpdated;
        public event OnErrorEventHandler OnError;

        #region Structs

        public struct ListFilters
        {
            public List<string> guilds;
            public List<string> worlds;
        }

        public struct PlayerRecord
        {
            public string name;
            public int points;
            public DateTime updated;
        }

        public struct ScheduledListUpdate
        {
            public DateTime last;
            public DateTime next;
        }

        public struct AchievementsPage
        {
            public Region region;
            public short page;
            public bool hasNext;
            public bool loggedIn;
            public List<PlayerRecord> records;
            public ListFilters filters;
            public ScheduledListUpdate updated;
        }

        #endregion

        private Session session;
        private int currentProcess;

        public Achievements(Session session)
        {
            this.session = session;
        }

        public void Abort()
        {
            lock (this)
            {
                currentProcess++;
            }
        }

        private int InnerValue(string data, string beggining, string ending, int startAt, out string value)
        {
            if (startAt < 0)
            {
                value = null;
                return startAt;
            }

            int index = data.IndexOf(beggining, startAt);
            if (index < 0)
            {
                value = null;
                return index;
            }

            index += beggining.Length;
            int end = data.IndexOf(ending, index);
            if (end < 0)
            {
                value = null;
                return end;
            }

            value = data.Substring(index, end - index);
            return end;
        }

        #region Requests (threaded getters)

        public void RequestAchievementsPage(Region r)
        {
            string region = (r == Region.EU) ? REGION_EU : REGION_NA;

            RequestAchievementsPage(new GetAchievementsPageDelegate(
                delegate(short page)
                {
                    return GetAchievementsPage(String.Format(ACHIEVEMENTS_URL, region), null, page);
                }), false);
        }

        private void RequestAchievementsPage(GetAchievementsPageDelegate get, bool followNext)
        {
            int id;
            lock (this)
            {
                id = ++currentProcess;
            }

            new Thread(new ThreadStart(delegate
            {
                short page = 1;
                byte errors = 0;
                bool hasNext = true;

                while (id == currentProcess && hasNext)
                {
                    try
                    {
                        AchievementsPage p = get(page);
                        page++;
                        errors = 0;

                        if (!followNext) //not following page links, override if next page is available
                            p.hasNext = false;

                        lock (this)
                        {
                            if (id == currentProcess)
                            {
                                if (OnAchievementsPageUpdated != null)
                                    OnAchievementsPageUpdated(p);
                            }
                        }

                        if (followNext)
                        {
                            if (hasNext = p.hasNext)
                                Thread.Sleep(0);
                        }
                        else
                            hasNext = false;
                    }
                    catch (ServerBusyException)
                    {
                        if (errors++ > ERROR_RETRY_LIMIT)
                        {
                            lock (this)
                            {
                                if (id == currentProcess)
                                {
                                    if (OnError != null)
                                        OnError(new TimeoutException());
                                }
                            }
                            return;
                        }
                        else
                        {
                            DateTime timeout = DateTime.Now.AddSeconds(ERROR_TIMEOUT_SECONDS);
                            while (id == currentProcess && DateTime.Now < timeout)
                            {
                                Thread.Sleep(100);
                            }
                        }
                    }
                    catch (AuthorizationException ae)
                    {
                        lock (this)
                        {
                            if (id == currentProcess)
                            {
                                if (OnError != null)
                                    OnError(ae);
                            }
                        }
                        return;
                    }
                }
            })).Start();
        }

        public void RequestWorld(Region region, string world)
        {
            RequestAchievementsPage(new GetAchievementsPageDelegate(
                delegate(short page)
                {
                    return GetWorld(region, world, page);
                }), true);
        }

        public void RequestGuild(Region region, string guild)
        {
            RequestAchievementsPage(new GetAchievementsPageDelegate(
                delegate(short page)
                {
                    return GetGuild(region, guild, page);
                }), true);
        }

        public void RequestFriends(Region region)
        {
            RequestAchievementsPage(new GetAchievementsPageDelegate(
                delegate(short page)
                {
                    return GetFriends(region, page);
                }), true);
        }

        #endregion

        #region Getters (page downloader)

        private AchievementsPage GetAchievementsPage(string url, string id, short page)
        {
            string data = this.Get(url);

            AchievementsPage p = new AchievementsPage();

            p.page = page;
            p.hasNext = (data.IndexOf("page=" + (page + 1)) >= 0);
            p.region = ParseRegion(data);
            p.records = ParseRecords(data, id);

            p.filters = ParseFilters(data);
            p.updated = ParseUpdated(data);
            p.loggedIn = ParseLoggedIn(data);

            return p;
        }

        private AchievementsPage GetWorld(Region r, string world, short page)
        {
            string region = (r == Region.EU) ? REGION_EU : REGION_NA;
            return GetAchievementsPage(String.Format(WORLD_URL, region, Uri.EscapeUriString(world), page), "world", page);
        }

        private AchievementsPage GetGuild(Region r, string guild, short page)
        {
            string region = (r == Region.EU) ? REGION_EU : REGION_NA;
            return GetAchievementsPage(String.Format(GUILD_URL, region, Uri.EscapeUriString(guild), page), "guild", page);
        }

        private AchievementsPage GetFriends(Region r, short page)
        {
            string region = (r == Region.EU) ? REGION_EU : REGION_NA;
            return GetAchievementsPage(String.Format(FRIENDS_URL, region, page), "friends", page);
        }

        private string Get(string url)
        {
            HttpWebRequest request;
            HttpWebResponse response;
            Stream stream;

            request = (HttpWebRequest)HttpWebRequest.Create(url);
            request.AllowAutoRedirect = true;
            request.Method = "GET";
            request.CookieContainer = new CookieContainer();
            request.CookieContainer.Add(session.Cookies);

            try
            {
                response = (HttpWebResponse)request.GetResponse();
                stream = response.GetResponseStream();
            }
            catch (WebException we)
            {
                HttpStatusCode code = ((HttpWebResponse)we.Response).StatusCode;
                if (code == HttpStatusCode.Unauthorized)
                {
                    session.Invalidate();
                    throw new AuthorizationException();
                }
                else if (code == HttpStatusCode.InternalServerError) //occurs when you request too much/fast
                    throw new ServerBusyException();

                return "";
            }
            catch
            {
                return "";
            }

            StreamReader reader = new StreamReader(stream);
            string data = reader.ReadToEnd();

            response.Close();
            reader.Close();
            reader.Dispose();

            return data;
        }

        #endregion

        #region Parsers

        private ScheduledListUpdate ParseUpdated(string data)
        {
            string lastUpdated, nextUpdate;
            int index = InnerValue(data, "timestamp\">Updated ", "</span", 0, out lastUpdated);
            index = InnerValue(data, "timestamp\">Next Update ", "</span", index, out nextUpdate);

            ScheduledListUpdate update = new ScheduledListUpdate();

            if (index >= 0)
            {
                update.last = ParseDate(lastUpdated);
                update.next = ParseDate(nextUpdate);
            }

            return update;
        }

        private bool ParseLoggedIn(string data)
        {
            return data.IndexOf("alue=\"friends\"") >= 0;
        }

        private Region ParseRegion(string data)
        {
            if (data.IndexOf("value=\"eu\" selected") >= 0)
                return Region.EU;

            return Region.NA;
        }

        private List<PlayerRecord> ParseRecords(string data, string id)
        {
            if (id == null)
                return null;

            List<PlayerRecord> records = null;

            int index = data.IndexOf("data-filter=\"" + id + "\"");
            if (index >= 0)
            {
                records = new List<PlayerRecord>();

                int last = data.IndexOf("</table>", index);
                do
                {
                    string name, lastUpdate, points;
                    index = InnerValue(data, "<td class=\"name text\">", "</td>", index, out name);
                    index = InnerValue(data, "<span class=\"cell-inner after-arrow\">", "</span>", index, out points);
                    index = InnerValue(data, "<span class=\"additional before-arrow\">", "</span>", index, out lastUpdate);

                    if (index >= 0)
                    {
                        PlayerRecord record = new PlayerRecord();
                        record.points = -1;
                        record.name = name.Trim();
                        record.updated = ParseSince(lastUpdate);
                        Int32.TryParse(points.Trim(), out record.points);

                        records.Add(record);
                    }
                }
                while (index >= 0 && index < last);
            }

            return records;
        }

        private ListFilters ParseFilters(string data)
        {
            ListFilters lists = new ListFilters();

            lists.guilds = ParseFilter(data, "guild");
            lists.worlds = ParseFilter(data, "world");

            return lists;
        }

        private List<string> ParseFilter(string data, string id)
        {
            List<string> items = new List<string>();

            int index = data.IndexOf("<select name=\"" + id + "\"");
            if (index >= 0)
            {
                int last = data.IndexOf("</select>", index);

                do
                {
                    index = data.IndexOf("<option value=", index);
                    if (index >= 0 && index < last)
                    {
                        index = data.IndexOf('>', index);
                        int end = data.IndexOf('<', index);


                        string name = data.Substring(index + 1, end - index - 1).Trim();
                        name = name.Replace("&#x27;", "'");

                        items.Add(name);
                    }
                }
                while (index >= 0 && index < last);
            }
            else
                return null;

            return items;
        }

        private DateTime ParseSince(string since)
        {
            int index = since.IndexOf("Since");
            if (index < 0)
                return DateTime.MinValue;

            since = since.Substring(index + 5);

            return ParseDate(since);
        }

        private DateTime ParseDate(string date)
        {
            date = date.Trim();
            date = date.Replace("&#x2F;", "/");
            date = date.Replace("PDT", "-0700");
            date = date.Replace("PST", "-0800");

            //note that the date on the NA leaderboards is off by +1 hour, as of april 2013

            try
            {
                return DateTime.ParseExact(date, "M/d/yy h:mm tt K", System.Globalization.CultureInfo.InvariantCulture);
            }
            catch { }

            try
            {
                date = date.Substring(0, date.LastIndexOf(' ')).Trim();
                return DateTime.ParseExact(date, "M/d/yy h:mm tt", System.Globalization.CultureInfo.InvariantCulture);
            }
            catch { }

            return DateTime.MinValue;
        }

        #endregion

        #region Exceptions

        public class TimeoutException : LeaderboardException
        {
            public TimeoutException()
                : base(ErrorCode.Timeout)
            {
            }
        }

        public class AuthorizationException : LeaderboardException
        {
            public AuthorizationException()
                : base(ErrorCode.NotAuthorized)
            {
            }
        }

        public class ServerBusyException : LeaderboardException
        {
            public ServerBusyException()
                : base(ErrorCode.ServerBusy)
            {
            }
        }

        public class LeaderboardException : Exception
        {
            public enum ErrorCode { Timeout, NotAuthorized, ServerBusy };
            
            public LeaderboardException(ErrorCode code)
            {
                this.Code = code;
            }

            public ErrorCode Code
            {
                get;
                set;
            }
        }

        #endregion
    }
}
